﻿using Abp;
using Abp.Auditing;
using Abp.Configuration.Startup;
using Abp.Domain.Repositories;
using Abp.Domain.Services;
using Abp.Domain.Uow;
using Abp.Extensions;
using Abp.Configuration;
using App.Authorization.Roles;
using App.Common;
using App.Configuration;
using App.MultiTenancy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Transactions;
using Abp.Timing;

namespace App.Authorization.Users
{
    public class UserManager : DomainService
    {
        public IAuditInfoProvider AuditInfoProvider { get; set; }

        private readonly IUnitOfWorkManager _unitOfWorkManager;
        private readonly IMultiTenancyConfig _multiTenancyConfig;
        private readonly IRepository<Tenant> _tenantRepository;
        private readonly IRepository<User, long> _userRepository;
        private readonly IRepository<Role> _roleRepository;
        private readonly IRepository<UserRole> _userRoleRepository;
        private readonly IRepository<LoginLog> _loginLogRepository;
        private readonly IRepository<MenuPermission> _menuRepository;
        private readonly IRepository<RolePermission> _rolePermissionRepository;
        private readonly IRepository<UserPermission> _userPermissionRepository;

        public UserManager(IRepository<User, long> userRepository,
            IRepository<UserRole> userRoleRepository,
            IRepository<Role> roleRepository,
            IRepository<LoginLog> loginLogRepository,
            IRepository<Tenant> tenantRepository,
            IRepository<MenuPermission> menuRepository,
            IRepository<RolePermission> rolePermissionRepository,
            IRepository<UserPermission> userPermissionRepository,
            IUnitOfWorkManager unitOfWorkManager,
            IMultiTenancyConfig multiTenancyConfig)
        {
            _unitOfWorkManager = unitOfWorkManager;
            _multiTenancyConfig = multiTenancyConfig;
            _tenantRepository = tenantRepository;
            _userRepository = userRepository;
            _userRoleRepository = userRoleRepository;
            _loginLogRepository = loginLogRepository;
            _menuRepository = menuRepository;
            _roleRepository = roleRepository;
            _rolePermissionRepository = rolePermissionRepository;
            _userPermissionRepository = userPermissionRepository;
        }

        public async Task<Tenant> GetTenantById(long? tenantId)
        {
            if (tenantId == null) return null;

            return await _tenantRepository.FirstOrDefaultAsync(s => s.Id == tenantId);
        }

        public virtual async Task<User> GetById(long userId)
        {
            return await Get(s => s.Id == userId);
        }

        public virtual async Task<User> GetByUserName(string userName)
        {
            return await Get(s => s.UserName == userName);
        }

        public virtual async Task<User> Get(Expression<Func<User, bool>> predicate)
        {
            return await _userRepository.FirstOrDefaultAsync(predicate);
        }

        public async Task<User> SaveOrUpdateUserAsync(User user)
        {
            return await _userRepository.InsertOrUpdateAsync(user);
        }

        public async Task DeleteAsync(long id)
        {
            await _userRepository.DeleteAsync(id);
        }

        public async Task<long> CountAsync(Expression<Func<User, bool>> predicate)
        {
            return await _userRepository.CountAsync(predicate);
        }

        public async Task SetRoles(User user, int[] roleIds)
        {
            //Remove from removed roles
            foreach (var userRole in await GetRolesByUserId(user.Id))
            {
                var role = await _roleRepository.GetAsync(userRole.Id);
                if (roleIds.All(roleId => role.Id != roleId))
                {
                    await RemoveFromRoleAsync(user.Id, role.Id);
                }
            }

            //Add to added roles
            foreach (var roleId in roleIds)
            {
                var role = await _roleRepository.GetAsync(roleId);
                if (user.Roles.All(ur => ur.RoleId != role.Id))
                {
                    await AddToRoleAsync(user.Id, roleId);
                }
            }
        }

        private async Task AddToRoleAsync(long userId, int roleId)
        {
            await _userRoleRepository.InsertAsync(new UserRole { UserId=userId, RoleId = roleId });
        }

        private async Task RemoveFromRoleAsync(long userId, int roleId) {
            await _userRoleRepository.DeleteAsync(s => s.UserId == userId && s.RoleId == roleId);
        }

        public async Task<List<MenuPermission>> GetAllMenuByUserId(long userId)
        {
            var user = await GetById(userId);
            if (user == null) return null;

            var userRoles = await GetRolesByUserId(user.Id);

            var data = new List<MenuPermission>();
            foreach (var role in userRoles)
            {
                var rolePermissions = _rolePermissionRepository.GetAllList(s => s.RoleId == role.Id);
                foreach (var menu in rolePermissions)
                {
                    var menuPermission = _menuRepository.FirstOrDefault(menu.MenuId);
                    if (menuPermission != null && data.Count(s => s.Id == menu.MenuId) == 0) data.Add(menuPermission);
                }
            }

            var userPermissions = _userPermissionRepository.GetAllList(s => s.UserId == userId);
            foreach (var menu in userPermissions)
            {
                var menuPermission = _menuRepository.FirstOrDefault(menu.MenuId);
                if (menuPermission != null && data.Count(s => s.Id == menu.MenuId) == 0) data.Add(menuPermission);
            }

            return data;
        }

        public async Task<List<Role>> GetRolesByUserId(long userId) {
            var userRoles = await _userRoleRepository.GetAllListAsync(s => s.UserId == userId);
            var userRoleIds = userRoles.Select(s => s.RoleId).ToArray();
            return await _roleRepository.GetAllListAsync(s=>userRoleIds.Contains(s.Id));
        }

        [UnitOfWork]
        public virtual async Task<LoginResult> LoginAsync(string userNameOrEmailAddress, string plainPassword, string tenancyName = null)
        {
            var result = await LoginAsyncInternal(userNameOrEmailAddress, plainPassword, tenancyName);
            await SaveLoginAttempt(result, tenancyName, userNameOrEmailAddress);
            return result;
        }

        private async Task<LoginResult> LoginAsyncInternal(string userNameOrEmailAddress, string plainPassword, string tenancyName)
        {
            if (userNameOrEmailAddress.IsNullOrEmpty())
            {
                throw new ArgumentNullException("userNameOrEmailAddress");
            }

            if (plainPassword.IsNullOrEmpty())
            {
                throw new ArgumentNullException("plainPassword");
            }

            //Get and check tenant
            Tenant tenant = null;
            using (_unitOfWorkManager.Current.SetTenantId(null))
            {
                if (!_multiTenancyConfig.IsEnabled)
                {
                    tenant = await GetDefaultTenantAsync();
                }
                else if (!string.IsNullOrWhiteSpace(tenancyName))
                {
                    tenant = await _tenantRepository.FirstOrDefaultAsync(t => t.TenancyName == tenancyName);
                    if (tenant == null)
                    {
                        return new LoginResult(LoginResultType.InvalidTenancyName);
                    }

                    if (!tenant.IsActive)
                    {
                        return new LoginResult(LoginResultType.TenantIsNotActive, tenant);
                    }
                }
            }

            var tenantId = tenant == null ? (int?)null : tenant.Id;
            using (_unitOfWorkManager.Current.SetTenantId(tenantId))
            {
                var user = await _userRepository.FirstOrDefaultAsync(s=>s.TenantId == tenantId && s.UserName == userNameOrEmailAddress);
                if (user == null)
                {
                    return new LoginResult(LoginResultType.InvalidUserNameOrEmailAddress, tenant);
                }

                var verificationResult = PasswordHash.ValidatePassword(plainPassword, user.PasswordHash);
                if (!verificationResult)
                {
                    return new LoginResult(LoginResultType.InvalidPassword, tenant);
                }

                return await CreateLoginResultAsync(user, tenant);
            }
        }

        private async Task<LoginResult> CreateLoginResultAsync(User user, Tenant tenant = null)
        {
            if (!user.IsActive)
            {
                return new LoginResult(LoginResultType.UserIsNotActive);
            }

            if (await IsEmailConfirmationRequiredForLoginAsync(user.TenantId) && !user.EmailConfirmed)
            {
                return new LoginResult(LoginResultType.UserEmailIsNotConfirmed);
            }

            user.LastLoginTime = Clock.Now;

            await _userRepository.UpdateAsync(user);

            await _unitOfWorkManager.Current.SaveChangesAsync();

            var usrRoles = await GetRolesByUserId(user.Id);

            return new LoginResult(LoginResultType.Success, tenant, user, usrRoles.Select(s=>s.RoleName).ToList());
        }

        private async Task SaveLoginAttempt(LoginResult loginResult, string tenancyName, string userNameOrEmailAddress)
        {
            var tenantId = loginResult.TenantId != null ? loginResult.TenantId : (int?)null;
            using (_unitOfWorkManager.Current.SetTenantId(tenantId))
            {
                var loginAttempt = new LoginLog
                {
                    TenantId = tenantId,
                    TenancyName = tenancyName,

                    UserId = loginResult.UserId != null ? loginResult.UserId : (long?)null,
                    UserNameOrEmailAddress = userNameOrEmailAddress,

                    Result = loginResult.Result,
                };

                //TODO: We should replace this workaround with IClientInfoProvider when it's implemented in ABP (https://github.com/aspnetboilerplate/aspnetboilerplate/issues/926)
                if (AuditInfoProvider != null)
                {
                    var auditInfo = new AuditInfo();
                    AuditInfoProvider.Fill(auditInfo);
                    loginAttempt.BrowserInfo = auditInfo.BrowserInfo;
                    loginAttempt.IpAddress = auditInfo.ClientIpAddress;
                    loginAttempt.ClientName = auditInfo.ClientName;
                }

                await _loginLogRepository.InsertAsync(loginAttempt);
                await _unitOfWorkManager.Current.SaveChangesAsync();
            }
        }

        private async Task<Tenant> GetDefaultTenantAsync()
        {
            var tenant = await _tenantRepository.FirstOrDefaultAsync(t => t.TenancyName == Tenant.DefaultTenantName);
            if (tenant == null)
            {
                throw new AbpException("There should be a 'Default' tenant if multi-tenancy is disabled!");
            }

            return tenant;
        }

        private async Task<bool> IsEmailConfirmationRequiredForLoginAsync(int? tenantId)
        {
            if (tenantId.HasValue)
            {
                return await SettingManager.GetSettingValueForTenantAsync<bool>(AppSettings.UserManagement.IsEmailConfirmationRequiredForLogin, tenantId.Value);
            }

            return await SettingManager.GetSettingValueForApplicationAsync<bool>(AppSettings.UserManagement.IsEmailConfirmationRequiredForLogin);
        }

        public class LoginResult
        {
            public LoginResult(LoginResultType result)
            {
                Result = result;
            }

            public LoginResult(LoginResultType result, Tenant tenant) : this(result)
            {
                Result = result;
                if(tenant !=null) TenantId = tenant.Id;
            }

            public LoginResult(LoginResultType result, Tenant tenant, User user, List<string> roles) : this(result, tenant)
            {
                Result = result;
                UserId = user.Id;
                if (tenant != null) TenantId = tenant.Id;
                Roles = roles;
            }

            public LoginResultType Result { get; set; }
            public long? UserId { get; set; }
            public int? TenantId { get; set; }
            public List<string> Roles { get; set; }
        }
    }
}